package high.concurrent.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import high.concurrent.constant.CacheKey;
import high.concurrent.dao.CouponMapper;
import high.concurrent.dto.NewUserDTO;
import high.concurrent.enums.BizCodeEnum;
import high.concurrent.enums.CouponCategoryEnum;
import high.concurrent.enums.CouponPublishEnum;
import high.concurrent.enums.CouponUseStatusEnum;
import high.concurrent.exception.BizException;
import high.concurrent.interceptor.UserInterceptor;
import high.concurrent.model.CouponDO;
import high.concurrent.model.CouponRecordDO;
import high.concurrent.model.LoginUser;
import high.concurrent.service.CouponRecordService;
import high.concurrent.service.CouponService;
import high.concurrent.utils.JsonData;
import high.concurrent.vo.CouponVO;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Service
public class CouponServiceImpl implements CouponService {

    @Autowired(required = false)
    private CouponMapper couponMapper;

    @Autowired(required = false)
    private CouponRecordService couponRecordService;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 查询所有已发布并且促销的优惠券
     * @param page  当前页
     * @param size  每页条数
     * @return
     */
    @Override
    public JsonData selectPublicsPromotionCouponList(Integer page, Integer size) {

        // 配置分页参数
        Page<CouponDO> pageInfo = new Page<>(page, size);

        // 分页查询已发布 并且 促销类的优惠券
        Page<CouponDO> couponDOPage = couponMapper.selectPage(pageInfo, new QueryWrapper<CouponDO>()
                .eq("publish", CouponPublishEnum.PUBLISH)
                .eq("category", CouponCategoryEnum.PROMOTION)
                // 按时间倒序
                .orderByDesc("create_time"));

        // 创建Map来存储需要的信息
        Map<String, Object> pageMap = new HashMap<>();
        // 总条数
        pageMap.put("total_record", couponDOPage.getTotal());
        // 总页数
        pageMap.put("total_page", couponDOPage.getPages());
        // 查询的数据,并转成VO返回给前端
        pageMap.put("current_data", couponDOPage.getRecords().stream()
                .map(couponDO ->couponDoToVo(couponDO))
                .collect(Collectors.toList()));

        return JsonData.buildSuccess(pageMap);
    }


    /**
     * 领取优惠券
     * @param couponId  优惠券id
     * @param couponCategoryEnum  领券类型限制
     * @return
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    @Override
    public JsonData receiveCoupon(Long couponId, CouponCategoryEnum couponCategoryEnum) {

        // 从threadLocal中获取解析token获取的用户信息，判断是否存在
        LoginUser loginUser = UserInterceptor.threadLocal.get();
        if(loginUser == null){
            throw new BizException(BizCodeEnum.ACCOUNT_UNLOGIN);
        }

        int rows;

        // 获取Redesson的锁对象(应该锁用户，防止用户超领。优惠券已经用stock > 0来约束了)
        String redissonLock = CacheKey.COUPON_RECEIVE_REDESSON_KEY + loginUser.getId() + couponId;
        RLock lock = redissonClient.getLock(redissonLock);

        // 加redesson分布式锁，设定过期时间是没有watch dog功能的，不能自动续期
//        lock.lock(20, TimeUnit.SECONDS);

        // 默认30过期，并配置watch dog线程，可以续期
        lock.lock();

        try {
            log.info("加锁成功");
            // 根据优惠券ID获取优惠券信息(id, 券类型, 是否发布)
            CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>()
                    .eq("id", couponId)
                    .eq("category", couponCategoryEnum)
                    .eq("publish", CouponPublishEnum.PUBLISH));

            if(couponDO == null){
                // 优惠券不存在
                throw new BizException(BizCodeEnum.COUPON_NO_EXITS);
            }

            // 校验用户是否符合领券要求
            this.checkCoupon(couponDO, loginUser.getId());

            // 符合领取优惠券条件, 优惠券库存数量-1(要在sql中进行减法)
            rows = couponMapper.reduceStock(couponId);

            // 扣减库存成功，则向领券表添加数据
            if(rows == 1){
                this.insertCouponRecord(couponDO, loginUser);
            }else{
                log.info("rows:{}, 发放优惠券失败: {}, 用户:{}", rows, couponDO,loginUser);
            }
        }finally {
             // 解锁
            lock.unlock();
        }

        return rows == 1 ? JsonData.buildSuccess() : JsonData.buildResult(BizCodeEnum.COUPON_GET_FAIL);
    }

    /**
     * 新用户注册发放优惠券
     * 1. 查询新人注册可以发放的所有优惠券
     * 2. 构造ThreadLocal
     * 3. 循环查询的优惠券，逐个添加
     * @param newUserDTO  用户信息
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public JsonData RPCNewCouponRecord(NewUserDTO newUserDTO, CouponCategoryEnum couponCategoryEnum) {

        // 查询出所有新注册类型的卡券, 如果是券没了，则不进行补发，所以要stock > 0
        List<CouponDO> listCategoryDOs = couponMapper.selectList(new QueryWrapper<CouponDO>()
                .eq("category", couponCategoryEnum.name())
                .gt("stock", 0));

        // 构造ThreadLocal,保存用户信息， 供【receiveCoupon】领取优惠券接口方法使用
        LoginUser loginUser = LoginUser.builder()
                .id(newUserDTO.getUserId())
                .name(newUserDTO.getName()).build();
        UserInterceptor.threadLocal.set(loginUser);

        // 逐条调用领券接口，需要【幂等性校验】 与 【加锁】。 但领券接口都做了，因此这里不需要做
        listCategoryDOs.stream().forEach(category -> {

            // 发送卡券，如果失败，则记录日志
            try {
                receiveCoupon(category.getId(), couponCategoryEnum);
            }catch (BizException e){
                log.info("卡券失败，准备后补");
                // TODO 异步写入日志表
            }
        });

        return JsonData.buildSuccess();
    }


    /**
     * 插入领券记录表
     * @param couponDO  优惠券信息
     * @param loginUser 用户token解析的信息
     */
    private void insertCouponRecord(CouponDO couponDO, LoginUser loginUser){

        // 插入领券表
        CouponRecordDO couponRecordDO = new CouponRecordDO();
        BeanUtils.copyProperties(couponDO, couponRecordDO);
        couponRecordDO.setUserId(loginUser.getId());
        couponRecordDO.setUserName(loginUser.getName());
        couponRecordDO.setUseState(CouponUseStatusEnum.NEW.toString());
        couponRecordDO.setCreateTime(new Date());
        couponRecordDO.setCouponId(couponDO.getId());
        // 会把couponDO的id拷贝进来，所以需要设置为Null，使用自增
        couponRecordDO.setId(null);

        couponRecordService.insertCouponRecord(couponRecordDO);
    }
    /**
     * 校验用户是否符合领券规则
     * @param couponDO 优惠券信息
     * @param userId    用户id
     */
    private void checkCoupon(CouponDO couponDO, Long userId) {

        // 通过couponId 和 userId去优惠券领取表查询
        List<CouponRecordDO> listCouponRecordDOs = couponRecordService.searchCouponRecordByUser(couponDO.getId(), userId);

        // 当前时间，优惠券开始时间，优惠券结束时间
        long newTime = System.currentTimeMillis();
        long startTime = couponDO.getStartTime().getTime();
        long endTime = couponDO.getEndTime().getTime();

        // 校验： 用户是否符合领取优惠券
        if(listCouponRecordDOs.size() >= couponDO.getUserLimit()){
            // 超领
            throw new BizException(BizCodeEnum.COUPON_OUT_OF_LIMIT);
        }else if(couponDO.getStock() <= 0){
            // 数量不足
            throw new BizException(BizCodeEnum.COUPON_NO_STOCK);
        }else if(newTime < startTime || newTime > endTime){
            // 不在领取时间范围
            throw new BizException(BizCodeEnum.COUPON_OUT_OF_TIME);
        }

    }


    /**
     * 优惠券的DO 转成 VO
     * @param couponDO  DO对象
     * @return
     */
    private CouponVO couponDoToVo(CouponDO couponDO){
        CouponVO couponVO = new CouponVO();
        BeanUtils.copyProperties(couponDO, couponVO);
        return couponVO;
    }

    /**
     * 原生redis+lua的分布式锁
     */
    private void redis_lua(){

//        // lua + redis实现原生分布式锁（加锁）
//        String uuid = CommonUtil.generateUUID();
//        String lockKey = "lock:coupon:" + couponId;
//        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 30, TimeUnit.SECONDS);
//        if(lockFlag){
//            try {
//                // TODO 加锁成功，执行核心业务
//
//            }finally {
//
//                // lua + redis实现原生分布式锁（删除锁）， 字符串固定写就行
//                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//                Integer result = (Integer) redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);
//                log.info("解锁: {}", result);
//            }
//        }else {
//            // 加锁失败，进行自旋，或者直接失败返回
//            try {
//                TimeUnit.SECONDS.sleep(1);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            // 自旋1秒重试
//            receiveCoupon(couponId, couponCategoryEnum);
//        }
    }
}
